﻿using System;
using System.Globalization;
using System.IO;
using System.Net;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using Ping.Core;
using log4net;

namespace Ping.Server.Jobs
{
	public sealed class GeoJob
		: Job
	{
		private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);

		private const string Host = "http://www.infosniper.net";
		private static readonly Regex PosExpr;
		private static readonly Regex GenericExpr;

		private readonly GeoCache _cache;

		static GeoJob()
		{
			PosExpr = new Regex("GLatLng\\(([-]?\\d+(\\.\\d*)?),([-]?\\d+(\\.\\d*)?)",
			                    RegexOptions.Compiled);

			// expression to capture a one cell of that fucking table
			// we'll simply count to retrieve the appropriate cell :D
			GenericExpr = new Regex("<td class=\"content-td2\">(.*?)</td>");
		}

		public GeoJob(GeoCache cache)
		{
			_cache = cache;
		}

		public override Type RequestType
		{
			get { return typeof (GeoRequest); }
		}

		public override Type ResultType
		{
			get { return typeof (GeoResult); }
		}

		public override string Name
		{
			get { return "Geolocate"; }
		}

		public override Result Perform(Request request)
		{
			var tmp = (GeoRequest) request;
			var result = new GeoResult
			             	{
			             		DataSets = new GeoDataSet[tmp.Addresses.Length],
			             	};

			if (Log.IsDebugEnabled)
				Log.DebugFormat("Performing geolation on {0} addresses", tmp.Addresses.Length);

			for (int i = 0; i < result.Count; ++i)
			{
				var address = tmp.Addresses[i];

				// Skip invalid addresses...
				if (!address.IsValid)
					continue;

				try
				{
					// Each individual address is being located, preferrably from the cache
					GeoDataSet dataSet = _cache.Lookup(address);
					if (dataSet == null)
					{
						if (Log.IsDebugEnabled)
							Log.DebugFormat("Retrieving GeoDataSet for {0} from the web", address);

						var webRequest = (HttpWebRequest) WebRequest.Create(MakeUri(address.Native));
						webRequest.Timeout = (int) tmp.Timeout.TotalMilliseconds;
						var response = (HttpWebResponse) webRequest.GetResponse();
						string content = ReadEntireStream(response);
						dataSet = ParseResult(content);

						_cache.Store(address, dataSet);
					}

					result.DataSets[i] = dataSet;
				}
				catch(FormatException e)
				{
					// TODO: Log
				}
				catch(WebException e)
				{
					// TODO: Log
				}
			}

			return result;
		}

		private string MakeUri(IPAddress address)
		{
			string str = string.Format("{0}/index.php?ip_address={1}", Host, address);
			return str;
		}

		private GeoDataSet ParseResult(string response)
		{
			Match match = PosExpr.Match(response);
			string tmp1 = match.Groups[1].Value;
			string tmp2 = match.Groups[3].Value;

			// InfoSniper.net uses the '.' to indicate the german ',' and
			// therefore the invariant culture must be used, in case the phone
			// doesn't use the "right" locale...
			double lat = double.Parse(tmp1, CultureInfo.InvariantCulture);
			double lon = double.Parse(tmp2, CultureInfo.InvariantCulture);

			var result = new GeoDataSet
			             	{
			             		Position = new GeoPosition {Longitude = lon, Latitude = lat}
			             	};

			int start = 0;
			int i = 0;
			while (true)
			{
				match = GenericExpr.Match(response, start);

				if (!match.Success)
					break;

				Group g = match.Groups[1];
				string value = g.Value;

				// This code will fail hard if the site changes its layout.
				// However this is html4 code and therefore I suspect they will change it when
				// hell is frozen...
				switch (i)
				{
					case 1:
						result.City = Readable(value);
						break;

					case 5:
						result.RegionName = Readable(value);
						break;

					case 9:
						result.CountryName = Readable(value);
						break;

					case 13:
						result.ContinentName = Readable(value);
						break;
				}

				// The next start...
				start = g.Index + g.Length;
				// index of the match...
				++i;
			}

			return result;
		}

		/// <summary>
		/// Strips non-readonly bullshit from the given string.
		/// </summary>
		/// <param name="source"></param>
		/// <returns></returns>
		private static string Readable(string source)
		{
			int i = source.IndexOf('<');
			if (i == -1)
				return source;

			return source.Substring(0, i);
		}

		/// <summary>
		/// Reads the entire result stream from the given response into a string assuming UTF8 encoding
		/// and returns it.
		/// </summary>
		/// <param name="response"></param>
		/// <returns></returns>
		private string ReadEntireStream(HttpWebResponse response)
		{
			Stream stream = response.GetResponseStream();
			if (stream == null)
				throw new InvalidOperationException("GetResponseStream returned null");

			var reader = new StreamReader(stream, Encoding.UTF8);
			var str = reader.ReadToEnd();
			return str;

			/*var outStream = new MemoryStream();
			var buffer = new byte[4096];

			while (outStream.Length < response.ContentLength)
			{
				int read = stream.Read(buffer, 0, buffer.Length);
				if (read == 0)
					break;

				outStream.Write(buffer, 0, read);
			}

			byte[] tmp = outStream.ToArray();
			return decoder.GetString(tmp, 0, tmp.Length);*/
		}
	}
}